var path = require('path');
var fs = require('fs');
var gm = require('gm');
var getPixels = require('get-pixels');
var config = require('./config');
var crypto = require('crypto');

const allowedSize = [80, 160, 320, 640, 1080, 1920];

var r_k = parseInt(Math.random() * 10000).toString(35), _uid = 1;

function resizeToN(src_img, dest_img, w, h) {
    return new Promise((resolve, reject) => {
        var im = gm(src_img);
        im.resize(w, h, '!').write(dest_img, function(err) {
            if (err) {
                reject(err);
            } else {
                getPixels(dest_img, function(err, pixels) {
                    if(err) {
                        reject(err);
                    } else {
                        resolve(pixels);
                    }
                })
            }
        });
    })
}

function getGrayPixels(pixels) {
    var xn = pixels.shape[0], yn = pixels.shape[1],
        data = pixels.data,
        plen = pixels.shape[2];
    var gray = new Uint8Array(xn * yn);

    for (var y = 0; y < yn; y++) {
        for (var x = 0; x < xn; x++) {
            var i = (x + y * xn), j = i * plen;
            gray[i] = (data[j+0] + data[j+1] + data[j+2]) /3;
        }
    }
    return gray;
}

function parseN(n) {
    var n = parseInt(n / 8) * 8;
    if (!n || n > 64) {
        throw 'required: 8 <= n <= 64';
    }
    return n;
}

function toHash(b_str) {
    var hash = '';
    for (var i = b_str.length; i > 0; i -= 4) {
        var s = Math.max(i - 4, 0), e = i;
        hash = parseInt(b_str.slice(s, e), 2).toString(16).toLowerCase() + hash;
    }
    return hash;
}

var hex_s = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
var DIS_TABLE = hex_s.reduce((a, b) => {
    var _count = function(c1, c2) {
        var n1 = parseInt(c1, 16), n2 = parseInt(c2, 16), c = 0, h = n1 ^ n2;
        while (h && c < 4) {
            h = (h - 1) & h;
            c++;
        }
        return c;
    };
    a[b] = hex_s.reduce((x, y) => {
        x[y] = _count(b, y);
        return x;
    }, {});
    return a;
}, {});

var HASH8_K_ARR = ["9eee99ffad6cb504","b4c5c6acb09e2268","ce72550de40b7ff5","3e3389a02ee393bd","82675baf9f5f5cbf","12e9095276dc8735","b7f8490bdd15cd8d","623b7e148246e55c","08d154b1a3ab0853","09d70e18165718b9","e402499222dfe5d0","e96f7bc10ed6676a","54a7eb4aace2f064","256201354302a154","3e7e47779798b040","0238a689c4b98994","c0d9ff71ad164024","1adcf1019734f1d2","013b7df56d1405cb","ff8fc1c3ab6aea8a","e609c0144246aa94","f74ed9a9ef058337","e27ccb7ba8d589e1","b01b3660f2e81547","571bab0adfaafe8e","27bdae945632125d","4f75380d139bdcb7","79cb98e4da3963fd","53829fcebc62f631","17f479153b68e96b","eedde7c9ca3a5611","1ae05f174724decc","165fcae70b491f7f","c32db5379327d8c8","c19cd9446091316f","7eb8a5229c46cbfd","57e2d8db3d3a7223","0b7a9530500eb7b6","2cd278972c2b698a","c32ce27291bc2574","9417cb192f032d0e","18e097a4fb980b71","70c43823b959b52c","c5159cde3545bbd7","90ffcb18762f3c87","99c6ad4f1440006c","59e437d029793491","577f25ee3b111f4f","c61c8f3e794fc006","d7d6cfb83cd10d2d","c8a50e677d5c3b18","962785c2b48960d3","5bf5d2e7e59d0bea","b014003996f990fc","16d6a2f709342acb","26a599e80d607146","9c38e7d8736ff51e","35ae2f7a170a8d87","3d317380acfbc91b","4404062b5cd52a53","861001e8ec8fcec9","4ae3c01b6a30029a","8c0cb4ddf38c9f3f","2bb3047a553d6358","8886040f5f53f6e8","dfc9871286a3b647","2f857cfccac07512","4e5fadcb18d38d4e","bb96644c876aac41","87a2a7f9d9ae734b","bb77249292061eb7","cc28c0b7b31dedc3","d9186a94b8321c2e","3a9abfd04afdb46f","192bab11c7f2e34f","157cd38385995639","1c1806a0efaf0542","fa3bf19a9e67d0d5","edd0467c4336501a","5589c4ab68bfe3cc","f04fb7145710e88b","7f0cb5be67dfa16a","1f7edacee36a2246","7a634ddc11497296","d43afbd6afa0addc","f6d464f28bcb00ee","f6dbf6c31e78389c","d10b752f7bd6b756","59ddcb92b77bdfdf","2248d225a2a5647e","d8a93d2253f26c4e","7c725b64b7205371","43fffffe88062ae2","6c7d1f2c52b496c8","590f7b996bdd1734","27fdd5da56986910","0f90a79199181c95","6252c40535bfe741","70d3bb3a42b8d8e2","acb748e280a2b39b","570400cd283e173b","841de57f5a6c7482","89a808615078c25f","9765b8a2cefe0ef6","3bc9a9fff1f723e4","ff00dacba8ee90c3","661451edcabbb494","c19177efcc92f2c8","2e69f3d8aa69532d","c8a7dedeaabc2414","24f17b69f0a98930","fdc6c4871a47484a","767b1d126ba0cbec","b98bb7854016bae5","9d91cc5f56993200","57435dbe22cc44c1","9ea3e84bbe380e01","ca56c729bf37255f","244cebdd134d4b1d","0876835c72e65613","5d87d520368a1f24","04044786072661ea","f77e0fc9867c0d7e","db443e2714bb9b37","103fcaaec790147b","7695e568d8521acb","cf69e7438b1dbf43","f9fe5e73700aa847"];

var HASH8_K_MD5 = "";

void function() {
    var t = crypto.createHash('md5');
    HASH8_K_ARR.forEach(x => t.update(x));
    HASH8_K_MD5 = t.digest('hex');
}();

var iArr = HASH8_K_ARR.reduce((arr, val, i) => {return arr.push(i), arr}, []);

module.exports = {
    toQueryObj: function(field, arr, di=8, n=15) {
        if (di < 0 || di > 10) {
            console.warn('di must between [0, 10]');
        }
        di = Math.max(Math.min(10, di), 0);
        n = Math.min(n, arr.length);
        // 只是缩小检索范围，选取n个基数
        return iArr.slice().sort((a, b) => {return Math.random() - 0.5;}).reduce((obj, index, i) => {
            var val = arr[index];
            return obj[`${field}.${index}`] = {$gte: Math.max(0, val - di), $lte: val + di}, obj;
        }, {});
    },
    K: {
        "HASH8": {
            arr: HASH8_K_ARR,
            md5: HASH8_K_MD5
        }
    },
    stime: Date.now(),
    randomHash(n) {
        var n = parseN(n);
        var t = "";
        for (var i = 0; i < n; i++) {
            t += Math.random() > 0.5 ? '1' : '0';
        }
        return toHash(t);
    },
    distanceOf: function(h1, h2) {
        // 返回两个哈希值的海明距离
        var c = 0, al = h1.length, bl = h2.length, s1, s2;
        var n = Math.max(al, bl);
        while (n-- > 0) {
            --al; --bl; 
            s1 = al >= 0 ? h1[al] : '0';
            s2 = bl >= 0 ? h2[bl] : '0';
            c += DIS_TABLE[s1][s2];
        }
        return c;
    },
    get_d_i: async function(hash) {
        var n = parseN(hash.length);

    },
    get_bHash: async function(src_img, n=8) {
        var n = parseN(n);
        var tempFile = path.join(config.tempFileDirectory, `_temp_${this.uniqueString()}.jpg`);
        var pixels = await resizeToN(src_img, tempFile, n + 1, n);
        var gray = getGrayPixels(pixels);
        var hash = '', t = '';
        for (var y = 0; y < n; y++) {
            for (var x = 1; x <= n; x++) {
                var i = y * (n + 1) + x;
                t += gray[i] <= gray[i-1] ? '1' : '0';
            }
        }
        hash = toHash(t);
        await this.unlinkFile(tempFile);
        return hash;
    },
    get_aHash: async function(src_img, n=8) {
        var n = parseN(n);
        var tempFile = path.join(config.tempFileDirectory, `_temp_${this.uniqueString()}.jpg`);
        var pixels = await resizeToN(src_img, tempFile, n, n);
        var gray = getGrayPixels(pixels);
        var m = gray.reduce((a, b) => a + b, 0) / gray.length;
        var hash = '', t = '';
        for (var i = 0; i < gray.length; i++) {
            t += gray[i] >= m ? '1' : '0';
        }
        hash = toHash(t);
        await this.unlinkFile(tempFile);
        return hash;
    },
    uniqueString: function() {
        return `${this.stime.toString(35)}z${r_k}z${(_uid++).toString(35)}`;
    },
    allowedSize,
    getImageSize: async function(src_img) {
        return new Promise((resolve, reject) => {
            var im = gm(src_img);
            im.size((err, size) => {
                if (err) {
                    return reject(err);
                }
                return resolve(size);
            })
        })
    },
    compress: async function(src_img, dest_img, width) {
        
        return new Promise((resolve, reject) => {
            var im = gm(src_img);
            im.resize(width).compress('WebP').write(dest_img, function(err) {
                if (err) {
                    reject(err);
                } else {
                    resolve(dest_img);
                }
            });
        });
    },
    crateThumb: async function(src_img, dest_img, size=150) {
        return new Promise((resolve, reject) => {
            gm(src_img).thumb(size, size, dest_img, 120, function(err) {
                if (err) {
                    reject(err);
                } else {
                    resolve(dest_img);
                }
            })
        })
    },
    compressAndGetPath: async function(src_img, width) {
        var [compressedFile, real_width] = this.getCompressedInfo(src_img, width);
        var existed = await this.isFileExists(compressedFile);
        if (existed) {
            return compressedFile;
        }
        var size = await this.getImageSize(src_img);
        if (size.width <= width) {
            return src_img;
        } else {
            await this.compress(src_img, compressedFile, real_width);
            return compressedFile;
        }
    },
    makeThumbIfNotExisted: async function(src_img) {
        var thumbFile = this.getThumbName(src_img);
        var existed = await this.isFileExists(thumbFile);
        if (existed) {
            return thumbFile;
        }
        return await this.crateThumb(src_img, thumbFile);
    },
    getCompressedInfo: function(src_img, width) {
        var len = this.allowedSize.length;
        for (var i = 1; i < len; i++) {
            if (width < this.allowedSize[i]) {
                width = this.allowedSize[i-1];
                break;
            }
        }
        width = Math.min(this.allowedSize[len-1], width);
        return [path.join(config.tempFileDirectory, `${path.basename(src_img)}@${width}${'.webp'}`), width];
    },
    getThumbName: function(src_img) {
        return path.join(config.thumbDirectory, path.basename(src_img) + '.jpg');
    },
    getOriginalName: function(img) {
        return path.join(config.imageDirectory, path.basename(img));
    },
    getErrorName: function() {
        return path.join(config.webpackBuildPath, 'error.png')
    },
    isFileExists: async function(file) {
        return new Promise((resolve) => {
            fs.exists(file, existed => {
                return resolve(existed);
            })
        })
    },
    copyFile: async function(src, dest) {
        return new Promise((resolve, reject) => {
            fs.createReadStream(src)
            .pipe(fs.createWriteStream(
                dest
            ))
            .on('close', function() {
                resolve();
            })
            .on('error', function(err) {
                reject(err);
            })
        })
    },
    unlinkFile: async function(src) {
        return new Promise((resolve, reject) => {
            fs.unlink(src, function(err) {
                if (err) {
                    reject(err);
                } else {
                    resolve();
                }
            })
        })
    },
    isSimilary: function([...hashArr], [...hashArr2], di=8) {
        for (var i = 0, len = hashArr.length; i < len; i++) {
            if (this.distanceOf(hashArr[i], hashArr2[i]) > di) {
                return false;
            }
        }
        return true;
    }
}